#include <syncup_db.h>
#include <types.h>
#include <common.h>

#include <pthread.h>

#include <mysql.h>
#include <glib.h> // GString

/*
 * Data structure to store the results cursor for comparison of two databases
 *
 * Fields       :
 * --------------
 * type         : Which syncup diff type (mov: 0, mod: 1, cre: 2, rem: 3)
 * results      : A pointer to MYSQL_RES type data.
 *
 */
struct syncup_db_cursor {
    int type;
    MYSQL_RES *results;
};

static MYSQL *mysql;
static pthread_mutex_t mysql_mutex = PTHREAD_MUTEX_INITIALIZER;

struct syncup_db_config {
    GString *defaults_file;
    GString *datadir;
};

static struct syncup_db_config config = {NULL, NULL};

static char *server_options[] = {
    "mysql_syncup",
    NULL,
    NULL,
    NULL
};

#define SERVER_OPTION_NUM ((sizeof(server_options) / sizeof(char *)) - 1)

static char *server_groups[] = { 
	"embedded",
    "server",
    NULL
};

//int num_elements = (sizeof(server_options) / sizeof(char *)) - 1;

/*
 * Constants for database names
 */
static const char *local_db = "syncuplocaldb";
static const char *remote_db = "syncupremotedb";

/*
 * Constants for database table names
 */
static const char *syncup_table = "syncup";


static void cleanup_mysql();
static void connect_mysql(MYSQL *mysql);
static int create_db_if_not_exist(MYSQL *mysql, const char *dbname);
static int select_db(MYSQL *mysql, const char *dbname);
static int create_table_if_not_exist(MYSQL *mysql, const char *tname);

static int
insert_root_if_not_exist(MYSQL *mysql, const char *tname)
{
    GString *sql = g_string_sized_new(1024);
    g_string_printf(sql, "INSERT IGNORE INTO %s VALUES ("
        "1, NULL, '%s', 0, NULL)", tname, g_get_home_dir());
    if (mysql_query(mysql, sql->str)) {
        fprintf(stderr, "Failed to create first row. Error: %s\n",
                mysql_error(mysql));
        return -1;
    }
    g_string_free(sql, TRUE);
    return 0;
}


// TODO: create my.cnf and data dir automatically if not exist

void
syncup_db_init(const char * syncup_home)
{
	GString *defaults_file_str;
    GString *datadir_str;

    config.defaults_file = g_string_new(syncup_home);
    g_string_append(config.defaults_file, "/my.cnf");
	defaults_file_str = g_string_new("--defaults-file=");
	g_string_append(defaults_file_str, config.defaults_file->str);

	config.datadir = g_string_new(syncup_home);
	g_string_append(config.datadir, "/data");
	datadir_str = g_string_new("--datadir=");
	g_string_append(datadir_str, config.datadir->str);
	
	server_options[1] = defaults_file_str->str;
	server_options[2] = datadir_str->str;

	// MySQL server initializations
	mysql_library_init(SERVER_OPTION_NUM, server_options, server_groups);
    mysql = mysql_init(NULL);
    mysql_options(mysql, MYSQL_READ_DEFAULT_GROUP, "embedded");
    mysql_options(mysql, MYSQL_OPT_USE_EMBEDDED_CONNECTION, NULL);
	connect_mysql(mysql);
    
    // Handle local_db selection
    create_db_if_not_exist(mysql, local_db);
    select_db(mysql, local_db);
    create_table_if_not_exist(mysql, syncup_table);
    insert_root_if_not_exist(mysql, syncup_table);
    
    g_string_free(defaults_file_str, TRUE);
    g_string_free(datadir_str, TRUE);
}

void
syncup_db_thread_init()
{
    mysql_thread_init();
}

void
syncup_db_thread_end()
{
    mysql_thread_end();
}

// TODO: unattach remote db

void 
attach_remote_db(const char *remote_db_path)
{
    GString *remote_db_path_buf = g_string_new(config.datadir->str);
    g_string_append_printf(remote_db_path_buf, "/%s", remote_db);
	if (access(remote_db_path_buf->str, F_OK) != 0) {
		if (symlink(remote_db_path, remote_db_path_buf->str) != 0) {
			fprintf(stderr, "Failed to create symlink to remote db!");
		}
	}
	g_string_free(remote_db_path_buf, TRUE);
}

void
syncup_db_end()
{
    mysql_library_end();
}

static void
cleanup_mysql() 
{
	mysql_close(mysql);
    //mysql_library_end();
}


static void
connect_mysql(MYSQL *mysql) 
{
    if (!mysql_real_connect(mysql, NULL, NULL, NULL, NULL, 0, NULL, 0)) {
        fprintf(stderr, "Failed to connect to database. Error: %s\n", 
        	mysql_error(mysql));
        cleanup_mysql();
    }
}


static int
create_db_if_not_exist(MYSQL *mysql, const char *dbname)
{
	GString *query_str;
    int ret;

	query_str = g_string_new("CREATE DATABASE IF NOT EXISTS ");
	g_string_append(query_str, dbname);
			
	if ((ret = mysql_query(mysql, query_str->str)) != 0) {
        fprintf(stderr, "Failed to create database. Error: %s\n", 
        	mysql_error(mysql));
        cleanup_mysql();
    }

	g_string_free(query_str, TRUE);
    return ret;
}


static int
select_db(MYSQL *mysql, const char *dbname)
{
	int ret;
	
	if ((ret = mysql_select_db(mysql, dbname))) {
        fprintf(stderr, "Failed to select database. Error: %s\n", 
        	mysql_error(mysql));
       cleanup_mysql();
    }
    
    return ret;
}


static int
create_table_if_not_exist(MYSQL *mysql, const char *tname)
{
	GString *query_str;
    int ret;

	query_str = g_string_sized_new(1024);
	g_string_printf(query_str, "CREATE TABLE IF NOT EXISTS %s ("
            "inode INTEGER AUTO_INCREMENT PRIMARY KEY, "
            "parent INTEGER, "
            "name CHAR(255), "
            "timestamp BIGINT, "
            "checksum BINARY(20), "
            "INDEX (parent))", tname);

	if ((ret = mysql_query(mysql, query_str->str)) != 0) {
        fprintf(stderr, "Failed to create table. Error: %s\n", 
        	mysql_error(mysql));
        cleanup_mysql();
    }
    
    g_string_free(query_str, TRUE);

	return ret;
}

static const char hex_digits[] = "0123456789abcdef";
static void digest_to_string(char *digest, size_t digest_len, char *buf)
{
    int i;
    for (i = 0; i < digest_len; ++i) {
        uint8_t byte = digest[i];
        buf[i * 2] = hex_digits[byte >> 4];
        buf[i * 2 + 1] = hex_digits[byte & 0xf];
    }
    buf[digest_len * 2] = '\0';
}

int
insert_new_tuple(struct syncup_db_entry *entry, int to, int checksum_null)
{
	GString *query_str;
	const gchar *db_name;

	int ret;

	if (to == 0) {
	    db_name = local_db;
	} else if (to == 1) {
	    db_name = remote_db;
	} else {
		 fprintf(stderr, "Insert flag not supported. Use 0 or 1!\n");
		 return -1;
	}

	query_str = g_string_sized_new(1024);
	g_string_printf(query_str, "INSERT INTO %s.syncup VALUES ("
	        "DEFAULT, %d, '%s', %ld, ",
	        db_name, entry->parent, entry->name, entry->timestamp);
	
	if (checksum_null) {
	    g_string_append(query_str, " NULL)");
	} else {
	    char sha1sum[sizeof (entry->checksum) * 2 + 1];
	    digest_to_string(entry->checksum, sizeof (entry->checksum), sha1sum);
		g_string_append_printf(query_str, " x'%s')", sha1sum);
	}
	
	g_debug("insert sql: %s", query_str->str);
	pthread_mutex_lock(&mysql_mutex);
	if ((ret = mysql_query(mysql, query_str->str)) != 0) {
        fprintf(stderr, "Failed to insert data. Error: %s\n",
                mysql_error(mysql));
	}
	entry->inode = (int) mysql_insert_id(mysql);
	pthread_mutex_unlock(&mysql_mutex);

	g_string_free(query_str, TRUE);
	return ret;
}


int
update_tuple(struct syncup_db_entry *key, struct syncup_db_entry *ent,
        int option, int in)
//update_tuple(struct syncup_db_entry *entry, int option, int in)
{
	GString *query_str;
	const gchar *update_str;	
	int ret;

	
	if (in == 0) {
		update_str = "UPDATE syncuplocaldb.syncup ";
	} else if (in == 1) {
		update_str = "UPDATE syncupremotedb.syncup ";
	} else {
		 fprintf(stderr, "Update flag not supported. Use 0 or 1!\n");
		 return -1;
	}
	
	query_str = g_string_new(update_str);
	g_string_append_printf(query_str, "SET ");

	if (option & UPDATE_PARENT) {
		g_string_append_printf(query_str, "parent = %d, ", ent->parent);
	}
	if (option & UPDATE_NAME) {
		g_string_append_printf(query_str, "name = '%s', ", ent->name);
	}
	if (option & UPDATE_TIMESTAMP) {
		g_string_append_printf(query_str, "timestamp = %ld, ",
		        ent->timestamp);
	}
	if (option & UPDATE_CHECKSUM) {
	    char sha1sum[sizeof (ent->checksum) * 2 + 1];
	    digest_to_string(ent->checksum, sizeof (ent->checksum), sha1sum);
		g_string_append_printf(query_str, "checksum = x'%s', ", sha1sum);
	}
	//int len = strlen(query_str->str);
	//g_string_erase(query_str, query_str->len - 2, 2);
	g_string_truncate(query_str, query_str->len - 2);

	g_string_append(query_str, " WHERE");
	if (option & WHERE_INODE) {
	    g_string_append_printf(query_str, " inode = %d AND", key->inode);
    }
    if (option & WHERE_PARENT) {
        g_string_append_printf(query_str, " parent = %d AND", key->parent);
    }
    if (option & WHERE_NAME) {
        g_string_append_printf(query_str, " name = '%s' AND", key->name);
    }
    g_string_truncate(query_str, query_str->len - 4);

    g_debug("update sql: %s", query_str->str);
    pthread_mutex_lock(&mysql_mutex);
	if ((ret = mysql_query(mysql, query_str->str)) != 0) {
        fprintf(stderr, "Failed to update data. Error: %s\n", 
        	mysql_error(mysql));	
	}
	pthread_mutex_unlock(&mysql_mutex);
	
	g_string_free(query_str, TRUE);
	return ret;
}


int
delete_tuple(struct syncup_db_entry *key, int option, int from)
{
	GString *query_str;
	const gchar *delete_str;
	int ret;

	if (from == 0) {
		delete_str = "DELETE FROM syncuplocaldb.syncup WHERE";
	} else if (from == 1) {
		delete_str = "DELETE FROM syncupremotedb.syncup WHERE";
	} else {
		fprintf(stderr, "Delete flag not supported. Use 0 or 1!\n");
		return -1;
	}

	query_str = g_string_new(delete_str);
	if (option & WHERE_INODE) {
	    g_string_append_printf(query_str, " inode = %d AND", key->inode);
	}
	if (option & WHERE_PARENT) {
	    g_string_append_printf(query_str, " parent = %d AND", key->parent);
	}
	if (option & WHERE_NAME) {
	    g_string_append_printf(query_str, " name = '%s' AND", key->name);
	}
	g_string_truncate(query_str, query_str->len - 4);

	g_debug("delete sql: %s", query_str->str);
	pthread_mutex_lock(&mysql_mutex);
	if ((ret = mysql_query(mysql, query_str->str)) != 0) {
        fprintf(stderr, "Failed to delete data. Error: %s\n", 
        	mysql_error(mysql));	
	}
	pthread_mutex_unlock(&mysql_mutex);

	g_string_free(query_str, TRUE);
	return ret;	
}


void
syncup_free_result(struct syncup_db_cursor *cursor)
{
    mysql_free_result(cursor->results);
	g_slice_free(struct syncup_db_cursor, cursor);
}

typedef void (*getter_t)(MYSQL_ROW, union syncup_db_collection *);

static void
get_moved_diff(MYSQL_ROW row, union syncup_db_collection *diff_data)
{
	diff_data->moved.inode = atoi(row[0]);
	diff_data->moved.from = atoi(row[1]);
	diff_data->moved.to = atoi(row[2]);
}

static void
get_modified_diff(MYSQL_ROW row, union syncup_db_collection *diff_data)
{
	diff_data->modified.inode = atoi(row[0]);
	diff_data->modified.parent = atoi(row[1]);
	diff_data->modified.name = row[2];

}

static void
get_created_diff(MYSQL_ROW row, union syncup_db_collection *diff_data)
{
	diff_data->created.inode = atoi(row[0]);
}

static void
get_removed_diff(MYSQL_ROW row, union syncup_db_collection *diff_data)
{
	diff_data->removed.inode = atoi(row[0]);
}

static void
get_entry(MYSQL_ROW row, union syncup_db_collection *diff_data)
{
	diff_data->entry.inode = atoi(row[0]);
	diff_data->entry.name = row[1];
}

static getter_t func_tbl[] = {
	get_moved_diff,
	get_modified_diff,
	get_created_diff,
	get_removed_diff,
	get_entry
};

int 
syncup_fetch_row(struct syncup_db_cursor *cursor,
        union syncup_db_collection *diff_data)
{
	MYSQL_ROW row;

	if ((row = mysql_fetch_row(cursor->results))) {
		func_tbl[cursor->type](row, diff_data);
		return 1;
	} else {
		return 0;
	}
}


struct syncup_db_cursor*
store_moved_tuples_result()
{
	struct syncup_db_cursor *c = g_slice_new(struct syncup_db_cursor);
	c->type = 0;

    pthread_mutex_lock(&mysql_mutex);
	mysql_query(mysql, 
		"SELECT L.inode, L.parent, R.parent "
		"FROM syncuplocaldb.syncup L, syncupremotedb.syncup R "
		"WHERE L.inode = R.inode AND L.parent <> R.parent");
	c->results = mysql_store_result(mysql);
    pthread_mutex_unlock(&mysql_mutex);

	if (c->results == NULL) {
		fprintf(stderr, 
			"Failed to retrive tuples in store_moved_tuples_result. Error: %s\n", 
			mysql_error(mysql));
	}
	
	return c;
}


struct syncup_db_cursor*
store_modified_tuples_result()
{
    MYSQL_RES *result;

	pthread_mutex_lock(&mysql_mutex);
	mysql_query(mysql, 
		"SELECT L.inode, L.parent, L.name "
		"FROM syncuplocaldb.syncup L, syncupremotedb.syncup R "
		"WHERE L.inode = R.inode AND L.checksum <> R.checksum");
	result = mysql_store_result(mysql);
	pthread_mutex_unlock(&mysql_mutex);

	if (result == NULL) {
		fprintf(stderr, 
			"Failed to retrive tuples in store_modified_tuples_result. "
			"Error: %s\n", mysql_error(mysql));
		return NULL;
	}

    struct syncup_db_cursor *c = g_slice_new(struct syncup_db_cursor);
    c->type = 1;
    c->results = result;
	return c;
}


struct syncup_db_cursor*
store_created_tuples_result()
{
	struct syncup_db_cursor *c = g_slice_new(struct syncup_db_cursor);
	c->type = 2;
	
    pthread_mutex_lock(&mysql_mutex);
	mysql_query(mysql, 
		"SELECT L.inode "
		"FROM syncuplocaldb.syncup L "
		"LEFT OUTER JOIN syncupremotedb.syncup R "
		"ON L.inode = R.inode "
		"WHERE R.inode IS NULL");
	c->results = mysql_store_result(mysql);
    pthread_mutex_unlock(&mysql_mutex);

	if (c->results == NULL) {
		fprintf(stderr, 
			"Failed to retrive tuples in store_created_tuples_result. "
			"Error: %s\n", mysql_error(mysql));
	}
	
	return c;
}


struct syncup_db_cursor*
store_removed_tuples_result()
{
	struct syncup_db_cursor *c = g_slice_new(struct syncup_db_cursor);
	c->type = 3;
	
    pthread_mutex_lock(&mysql_mutex);
	mysql_query(mysql, 
		"SELECT R.inode "
		"FROM syncuplocaldb.syncup L "
		"RIGHT OUTER JOIN syncupremotedb.syncup R "
		"ON L.inode = R.inode "
		"WHERE L.inode IS NULL");
	c->results = mysql_store_result(mysql);
    pthread_mutex_unlock(&mysql_mutex);

	if (c->results == NULL) {
		fprintf(stderr, 
			"Failed to retrive tuples in store_removed_tuples_result. "
			"Error: %s\n", mysql_error(mysql));
	}
	
	return c;
}


struct syncup_db_cursor*
store_entry_tuples_result(int inode)
{
	struct syncup_db_cursor *c = g_slice_new(struct syncup_db_cursor);
	c->type = 4;
	
	GString *query_str;
	const gchar *select_str;
	
	select_str = "SELECT L.inode, L.name "
				 "FROM syncuplocaldb.syncup L "
				 "WHERE L.checksum IS NULL AND L.parent = ";

	query_str = g_string_new(select_str);		 
	g_string_append_printf(query_str, "%d", inode);		 
	
    pthread_mutex_lock(&mysql_mutex);
	if (mysql_query(mysql, query_str->str) != 0) {
        fprintf(stderr, "Failed to select entry data. Error: %s\n", 
        	mysql_error(mysql));	
	}
	c->results = mysql_store_result(mysql);
    pthread_mutex_unlock(&mysql_mutex);

	if (c->results == NULL) {
		fprintf(stderr, 
			"Failed to retrive tuples in store_removed_tuples_result. "
			"Error: %s\n", mysql_error(mysql));
	}
	
	g_string_free(query_str, TRUE);
	
	return c;
}

int
syncup_db_test_thread_safe()
{
    return (int) mysql_thread_safe();
}

void
syncup_db_lock_query()
{
    pthread_mutex_lock(&mysql_mutex);
}

void
syncup_db_unlock_query()
{
    pthread_mutex_unlock(&mysql_mutex);
}
